# FeedStore.py
#
# Copyright (c) 2006 Michael Hobbs
#
# This file is part of iTorrent.
#
# iTorrent is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# iTorrent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with iTorrent; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA


import os
import time
from datetime import datetime
import md5

import yaml


FILE_NAME = 'feeds.yaml'
NEW = '.new'
ISO_FORMAT = '%Y-%m-%d %H:%M:%S'


class FeedStore(object):
	def __init__(self, dirName, rawServer):
		super(FeedStore, self).__init__()
		self.rawServer = rawServer
		self.savePending = False
		self.records = {}
		self.md5s = {}
		self.fileName = os.path.join(dirName, FILE_NAME)
		if not os.path.exists(self.fileName) and os.path.exists(self.fileName + NEW):
			# Previous process must have been killed before .new file was renamed.
			# (highly unlikely, but possible)
			os.rename(self.fileName + NEW, self.fileName)
		if os.path.exists(self.fileName):
			records = yaml.loadFile(self.fileName).next()
			for record in [Record(self, dict) for dict in records]:
				url = record.url
				self.records[url] = record
				self.md5s[md5.new(url).hexdigest()] = url
			# end for
		# end if

	def readRecords(self, records):
		for dict in records:
			record = Record(self, dict)
			self.records[record.url] = record
		# end for

	def save(self):
		if self.savePending:
			return
		self.savePending = True
		self.rawServer.add_task(self.doSave, 0)

	def doSave(self):
		f = file(self.fileName + NEW, 'wb')
		values = self.records.values()
		values.sort(sortByChannelTitle)
		dumper = yaml.Dumper()
		dumper.setSort(['channel', 'title'])
		dumper.dumpToFile(f, values)
		f.close()
		if os.path.exists(self.fileName):
			os.remove(self.fileName)
		os.rename(self.fileName + NEW, self.fileName)
		self.savePending = False

	def __len__(self):
		return len(self.records)

	def __getitem__(self, key):
		url = self.md5s.get(key, key)
		return self.records[url]

	def get(self, key, x=None):
		url = self.md5s.get(key, key)
		return self.records.get(url, x)

	def __setitem__(self, url, record):
		record.store = self
		self.records[url] = record
		self.md5s[md5.new(url).hexdigest()] = url
		self.save()

	def keys(self):
		return self.records.keys()

	# TODO: who uses this? Accept both a hash and a url.
	def __delitem__(self, url):
		del self.records[url]
		del self.md5s[md5.new(url).hexdigest()]
		self.save()

	def __contains__(self, key):
		url = self.md5s.get(key, key)
		return url in self.records

	def __str__(self):
		return str(self.records)

	def __iter__(self):
		return self.records.__iter__()


class Record(object):
	def __init__(self, *args, **kwargs):
		super(Record, self).__init__()
		super(Record, self).__setattr__('initializing', True)
		self.store = None
		if isinstance(args[0], FeedStore):
			self.initDict(*args, **kwargs)
		else:
			self.initParams(*args, **kwargs)
		super(Record, self).__setattr__('initializing', False)

	def initDict(self, store, dict):
		self.store = store
		# Required fields
		self.url = dict['url']
		self.length = dict['length']
		self.channel = dict['channel']
		self.title = dict['title']
		# Optional fields
		self.partialPath = dict.get('partial-path', dict.get('partial-file'))
		self.torrentFile = dict.get('torrent-file')
		self.published = parseTime(dict.get('published'))
		self.lastSeen = parseTime(dict.get('last-seen'))
		if not self.lastSeen:
			self.lastSeen = time.time()
		self.sent = parseTime(dict.get('sent'))
		self.numFailures = dict.get('num-failures', 0)

	def initParams(self, url, length, channel, title):
		self.url = url
		self.length = length
		self.channel = channel
		self.title = title
		self.published = None
		self.partialPath = None
		self.torrentFile = None
		self.lastSeen = time.time()
		self.sent = None
		self.numFailures = 0

	def __setattr__(self, name, value):
		super(Record, self).__setattr__(name, value)
		if not self.initializing and self.store and name != 'store':
			# Persist the change
			self.store.save()
		# end if

	def to_yaml(self):
		result = {}
		set(result, 'url', self.url)
		set(result, 'length', self.length)
		set(result, 'channel', self.channel)
		set(result, 'title', self.title)
		# TODO: remove partial-file after a few revisions.
		set(result, 'partial-file', self.partialPath)
		set(result, 'partial-path', self.partialPath)
		set(result, 'torrent-file', self.torrentFile)
		setDT(result, 'published', self.published)
		setDT(result, 'last-seen', self.lastSeen)
		setDT(result, 'sent', self.sent)
		set(result, 'num-failures', self.numFailures)
		return (result, None)


def set(dict, name, value):
	if value is None or value is False:
		return
	dict[name] = value

def setDT(dict, name, value):
	if value is None:
		return
	dt = datetime.fromtimestamp(value).replace(microsecond=0)
	dict[name] = dt.isoformat(' ')

def parseTime(timeString):
	if not timeString:
		return None
	return time.mktime(time.strptime(timeString, ISO_FORMAT))

def sortByChannelTitle(a, b):
	r = cmp(a.channel.lower(), b.channel.lower())
	if r != 0:
		return r
	return cmp(a.title.lower(), b.title.lower())
